﻿using System;
using T0yK4T.Tools;
using T0yK4T.Cryptography;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using MongoDB;
using T0yK4T.Configuration;
using T0yK4T.Data;
using System.Reflection;
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;

namespace ToyChat.Server
{
    /// <summary>
    /// Managed connections to the database
    /// </summary>
    public class DatabaseHandler : ComponentBase, IDatabaseHandler
    {
        private MongoDatabase database;
        private ConfigurableValue<string> mongoPath = Config.GetValue<string>("mongopath", new StringConverter(), string.Format("{0}\\mongodb", Environment.CurrentDirectory));
        
        private MongoCollection<UserInfo> userCollection;
        private MongoCollection<Channel> channelCollection;
        private Dictionary<Type, object> openCollections = new Dictionary<Type, object>();

        /// <summary>
        /// Initializes a new instance of <see cref="DatabaseHandler"/> using the property values specified in the supplied <see cref="ServerSettings"/>
        /// </summary>
        /// <param name="settings">The settings of the server that owns this handler</param>
        /// <param name="server">The server that owns this handler</param>
        public DatabaseHandler(IServerSettings settings, IServer server)
        {
            MongoConnectionStringBuilder builder = new MongoConnectionStringBuilder();
            builder.Username = settings.DatabaseUser;
            builder.DatabaseName = settings.DatabaseName;
            builder.Password = settings.DatabasePass;
            builder.Server = new MongoServerAddress(settings.DatabaseHost, settings.DatabasePort);
            
            MongoUrlBuilder uBuilder = new MongoUrlBuilder();
            uBuilder.DatabaseName = settings.DatabaseName;
            
            if (!string.IsNullOrEmpty(settings.DatabaseUser))
                uBuilder.DefaultCredentials = new MongoCredentials(settings.DatabaseUser, settings.DatabasePass);
            uBuilder.Server = new MongoServerAddress(settings.DatabaseHost, settings.DatabasePort);
            uBuilder.ConnectionMode = ConnectionMode.Direct;
            

            MongoServer serv = MongoServer.Create(uBuilder.ToMongoUrl());
            //MongoServer serv = new MongoServer(builder.ToServerSettings());

            //this.dbCreds = new MongoCredentials(settings.DatabaseUser, settings.DatabasePass);
            //Mongo m = new Mongo(builder.ToString());
            try { serv.Connect(); }
            catch (Exception e)
            {
                base.LogException(e, MethodBase.GetCurrentMethod());
                MongoWrapper wrapper = new MongoWrapper();
                if (!wrapper.MongoAvailable)
                    wrapper.StartMongo(mongoPath);
                if (!wrapper.MongoAvailable)
                    throw new InvalidOperationException("Unable to connect to database server");
                else
                {
                    try { serv.Connect(); }
                    catch (Exception er) { base.LogException(er, MethodBase.GetCurrentMethod()); throw; }
                }
            }
            this.database = serv.GetDatabase(settings.DatabaseName);//, new MongoCredentials(settings.DatabaseUser, settings.DatabasePass));
            this.Init();
        }

        private void Init()
        {
            this.userCollection = this.database.GetCollection<UserInfo>(typeof(UserInfo).FullName);
            this.channelCollection = this.database.GetCollection<Channel>(typeof(Channel).FullName);
            this.userCollection.CreateIndex(new IndexKeysDocument("Username", new BsonInt32(1)), new IndexOptionsDocument(false));
            this.userCollection.CreateIndex(new IndexKeysDocument("Email", new BsonInt32(1)), new IndexOptionsDocument(false));
            //this.userCollection.Metadata.CreateIndex(new Document("Username", 1), true);
            //this.userCollection.Metadata.CreateIndex(new Document("Email", 1), true);

            this.channelCollection.CreateIndex(new IndexKeysDocument("ChannelID", new BsonInt32(1)), new IndexOptionsDocument(false));
            this.channelCollection.CreateIndex(new IndexKeysDocument("ChannelName", new BsonInt32(1)), new IndexOptionsDocument(false));
            //this.channelCollection.Metadata.CreateIndex(new Document("ChannelID", 1), true);
            //this.channelCollection.Metadata.CreateIndex(new Document("ChannelName", 1), true);
        }

        /// <summary>
        /// Gets a unique hash for the specified string (Password)
        /// </summary>
        /// <param name="password"></param>
        /// <returns></returns>
        public string GetPassHash(string password)
        {
            return BitConverter.ToString(CryptoCommon.Hasher.ComputeHash(Encoding.Unicode.GetBytes(password)));
        }

        /// <summary>
        /// Attempts to find a user in the database using the specified <see cref="UserInfo"/> as a search template
        /// </summary>
        /// <param name="username">The username of the user to look for</param>
        /// <returns></returns>
        public IUserInfo FindUser(string username)
        {
            return this.userCollection.FindOne(Query.EQ("Username", new BsonString(username)));
            //return this.userCollection.FindOne((t) => t.Username.ToLower() == username.ToLower());
        }

        /// <summary>
        /// <see cref="IDatabaseHandler.GetChannels"/>
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Channel> GetChannels()
        {
            return this.channelCollection.FindAll();
        }

        /// <summary>
        /// Attempts to find a channel in the database with the specified ID
        /// </summary>
        /// <param name="channelID">The ID of the channel to find</param>
        /// <returns>The channel if it is found, otherwise null</returns>
        public IChannel FindChannel(int channelID)
        {
            return this.channelCollection.FindOne(Query.EQ("ChannelID", new BsonInt32(channelID)));
            //return this.channelCollection.FindOne((t) => t.ChannelID == channelID);
        }

        /// <summary>
        /// Attempts to find a channel with the specified name in the database
        /// </summary>
        /// <param name="channelName">The channel to find</param>
        /// <returns>The channel if found, otherwise null</returns>
        public IChannel FindChannel(string channelName)
        {
            return this.channelCollection.FindOne(Query.EQ("ChannelName", new BsonString(channelName)));
            //return this.channelCollection.FindOne((t) => t.ChannelName == channelName);
        }

        /// <summary>
        /// Stores or Updates the specified user in the database
        /// </summary>
        /// <param name="info">The informations for the user to be stored</param>
        public void StoreUser(IUserInfo info)
        {
            if (info == null)
                throw new ArgumentNullException("info");
            UserInfo check = this.userCollection.FindOne(Query.Or(Query.EQ("Username", info.Username), Query.EQ("Email", info.Email)));
            UserInfo toStorage = new UserInfo(info);
            if (check != null)
                toStorage._id = check._id;
            this.userCollection.Save(toStorage);
        }

        /// <summary>
        /// Deletes the specified user info from the database
        /// </summary>
        /// <param name="info">The info of the user to delete</param>
        public void DeleteUser(IUserInfo info)
        {
            if (info == null)
                throw new ArgumentNullException("info");
            this.userCollection.Remove(Query.EQ("Username", info.Username));
            //this.userCollection.Remove((t) => t.Username == info.Username);
        }

        /// <summary>
        /// Stores or Updates the specified channel in the database
        /// </summary>
        /// <param name="channel">The channel to store</param>
        public void StoreChannel(IChannel channel)
        {
            if (channel == null)
                throw new ArgumentNullException("channel");
            Channel check = this.channelCollection.FindOne(Query.Or(Query.EQ("ChannelName", channel.ChannelName), Query.EQ("ChannelID", channel.ChannelID)));
            //this.channelCollection.FindOne((t) => t.ChannelName == channel.ChannelName || t.ChannelID == channel.ChannelID);
            Channel toStorage = new Channel(channel);
            if (check != null)
                toStorage._id = check._id;
            this.channelCollection.Save(toStorage);
        }

        /// <summary>
        /// Deletes the specified channel from the database
        /// </summary>
        /// <param name="channel">The channel to delete</param>
        public void DeleteChannel(IChannel channel)
        {
            if (channel == null)
                throw new ArgumentNullException("channel");
            this.channelCollection.Remove(Query.Or(Query.EQ("ChannelName", channel.ChannelName), Query.EQ("ChannelID", channel.ChannelID)));
            //this.channelCollection.Remove((t) => t.ChannelName == channel.ChannelName || t.ChannelID == channel.ChannelID);
        }

        /// <summary>
        /// Gets an <see cref="MongoCollection{T}"/> for the specified type
        /// </summary>
        /// <typeparam name="T">The Type to get the collection for</typeparam>
        /// <returns>An <see cref="MongoCollection{T}"/> for the specified type</returns>
        public MongoCollection<T> GetCollection<T>() where T : class
        {
            object col;
            Type t = typeof(T);
            if (!this.openCollections.TryGetValue(t, out col))
            {
                col = this.database.GetCollection<T>(t.FullName);
                this.openCollections.Add(t, col);
            }
            return col as MongoCollection<T>;
        }
    }
}
